package implements

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"gitee.com/bitwormhole/cloud-aliyun-api/aliyun/openapi"
	"github.com/starter-go/base/lang"
	"github.com/starter-go/httpagent"
	"github.com/starter-go/vlog"
)

type openAPIClient struct {
	parent    *AliyunOpenAPIService
	config    *openapi.Configuration
	userAgent httpagent.Client
}

func (inst *openAPIClient) _impl() openapi.Client {
	return inst
}

func (inst *openAPIClient) Execute(ctx context.Context, task openapi.Task) error {

	// prepare openapi request
	req1 := task.PrepareRequest()
	req1.Context = ctx
	err := inst.completeOpenapiRequest(req1)
	if err != nil {
		return err
	}

	// create web request
	cfg := inst.config
	rb := new(openapiCanonicalRequestBuilder)
	rb.AccessKeyID = cfg.AccessKeyID
	rb.AccessKeySecret = []byte(cfg.AccessKeySecret)

	err = rb.init(req1)
	if err != nil {
		return err
	}

	req2, err := rb.create(ctx)
	if err != nil {
		return err
	}

	// execute
	resp2, err := inst.userAgent.Execute(req2)
	if err != nil {
		inst.handleErrorResponse(resp2)
		return err
	}

	// handle response
	req1.WebRequest = req2
	req1.WebResponse = resp2
	return task.HandleResponse(req1)
}

func (inst *openAPIClient) handleErrorResponse(resp *httpagent.Response) error {
	ct := resp.Headers.Get("content-type")
	if strings.Contains(ct, "application/json") {
		body, err := resp.GetEntity()
		if err != nil {
			return err
		}
		text, err := body.ReadText()
		if err != nil {
			return err
		}
		vlog.Warn("http response error: %s", text)
	}
	return nil
}

func (inst *openAPIClient) completeOpenapiRequest(req *openapi.Request) error {

	cfg := inst.config
	endpoint := req.Endpoint
	protocol := req.Protocol
	action := req.Action
	style := req.Style
	method := req.HTTPMethod
	var err error

	if protocol == "" {
		protocol = "https"
	}

	if endpoint == "" {
		endpoint = cfg.Endpoint
	}

	if style == "" {
		style = openapi.StyleRPC
	}

	if method == "" {
		method = http.MethodGet
	}

	if action != "" {
		req.SetParam("Action", action)
	}

	req.Endpoint = endpoint
	req.Protocol = protocol
	req.HTTPMethod = method
	req.Style = style

	switch style {
	case openapi.StyleRPC:
		err = inst.completeOpenapiRequestAsRPC(req)
	case openapi.StyleROA:
		err = inst.completeOpenapiRequestAsROA(req)
	default:
	}
	if err != nil {
		return nil
	}

	err = inst.completeOpenapiRequestHeaders(req)
	if err != nil {
		return nil
	}

	return nil
}

// func (inst *openAPIClient) computePayloadSum(req *openapi.Request) string {
func computePayloadSum(req *openapi.Request) string {
	data := req.RequestBody
	if data == nil {
		data = make([]byte, 0)
	}
	sum := sha256.Sum256(data)
	hex := lang.HexFromBytes(sum[:])
	return hex.String()
}

func (inst *openAPIClient) completeOpenapiRequestHeaders(req *openapi.Request) error {

	date := inst.makeDate(time.Now().Add(-time.Minute))
	action := req.Action
	host := req.Endpoint
	ver := req.APIVersion
	nonce := inst.makeNonce()
	payloadSum := computePayloadSum(req)

	if ver == "" {
		ver = "2015-12-15"
	}

	req.SetHeader(openapi.XACSAction, action)
	req.SetHeader(openapi.XACSVersion, ver)
	req.SetHeader(openapi.XACSDate, date)
	req.SetHeader(openapi.Host, host)
	req.SetHeader(openapi.XACSSignatureNonce, nonce)
	req.SetHeader(openapi.XACSContentSHA256, payloadSum)
	// req.SetHeader(openapi.XACSSecurityToken, "")

	return nil
}

func (inst *openAPIClient) makeNonce() string {
	data := make([]byte, 16)
	rand.Read(data)
	hex := lang.HexFromBytes(data)
	return hex.String()
}

func (inst *openAPIClient) makeDate(t time.Time) string {
	t = t.UTC()
	date := t.Format(time.RFC3339)
	if strings.Contains(date, "Z") {
		iz := strings.Index(date, "Z")
		date = date[0 : iz+1]
	} else if strings.Contains(date, "+") {
		iz := strings.Index(date, "+")
		date = date[0:iz] + "Z"
	}
	return date
}

func (inst *openAPIClient) completeOpenapiRequestAsROA(req *openapi.Request) error {

	return fmt.Errorf("no impl: completeOpenapiRequestAsROA")
}

func (inst *openAPIClient) completeOpenapiRequestAsRPC(req *openapi.Request) error {

	params := req.Params
	method := req.HTTPMethod

	if method == http.MethodGet {
		dst := req.QueryParameters
		for name, value := range params {
			dst = append(dst, name+"="+value)
		}
		req.QueryParameters = dst
	} else if method == http.MethodPost {
		urlencoded := url.Values{}
		for name, value := range params {
			urlencoded.Add(name, value)
		}
		data := urlencoded.Encode()
		req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
		req.QueryParameters = nil
		req.RequestBody = []byte(data)
	}

	return nil
}
